package base;

import java.util.LinkedList;
import java.util.List;

import org.jdom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import util.XMLWriter;
import cbr.metrics.MetricBuildings;
import cbr.metrics.MetricResources;
import cbr.metrics.MetricTroops;

public class WargusGameState {
	WargusMap m_map = null;
	List<WargusPlayer> m_opponents = null;
	WargusPlayer m_player = null;
	
	// For similarity purposes:
	MetricBuildings m_metrics_selfB = null;
	MetricBuildings m_metrics_oppB = null;
	MetricResources m_metrics_selfR = null;
	MetricResources m_metrics_oppR = null;
	MetricTroops m_metrics_selfT = null;
	MetricTroops m_metrics_oppT = null;	
	
	public WargusGameState(WargusMap map, List<WargusPlayer> opponents, WargusPlayer player) {
		m_map = new WargusMap(map);
		m_opponents = new LinkedList<WargusPlayer>();
		m_opponents.addAll(opponents); 
		m_player = player; 
		
		m_map.insertPlayerUnits(m_player.getUnits()); 
		for(WargusPlayer p:m_opponents) m_map.insertOpponentUnits(p.getUnits());
		
		m_metrics_selfB = null;
		m_metrics_oppB = null;
		m_metrics_selfR = null;
		m_metrics_oppR = null;
		m_metrics_selfT = null;
		m_metrics_oppT = null;					
	}
	
	public WargusGameState(WargusGameState wgs) {
		m_map = new WargusMap(wgs.m_map);
		m_opponents = new LinkedList<WargusPlayer>();
		for (WargusPlayer p : wgs.m_opponents) m_opponents.add(new WargusPlayer(p));
		m_player = new WargusPlayer(wgs.m_player);

		// XXX hmm is this the right thing to do?
		m_metrics_selfB = null;
		m_metrics_oppB = null;
		m_metrics_selfR = null;
		m_metrics_oppR = null;
		m_metrics_selfT = null;
		m_metrics_oppT = null;
	}
	
	public WargusUnit getUnit(int ID) {
		for(WargusPlayer p:m_opponents) {
			for(WargusUnit u:p.getUnits()) if (u.getUnitID()==ID) return u;
		}
		for(WargusUnit u:m_player.getUnits()) if (u.getUnitID()==ID) return u;
		for(WargusUnit u:m_map.get_goldMines()) if (u.getUnitID()==ID) return u;
		for(WargusUnit u:m_map.get_oilPatches()) if (u.getUnitID()==ID) return u;
		
		return null;
	}
	
	public WargusPlayer getPlayer(int ID) {
		for(WargusPlayer p:m_opponents) {
			if (p.getID()==ID) return p;
		}
		if (m_player.getID()==ID) return m_player;
		
		return null;
	}	
	
	public List<WargusUnit> getSelfUnits() {
		return m_player.getUnits();		
	}
	
	public List<WargusUnit> getEnemyUnits() {
		List<WargusUnit> l = new LinkedList<WargusUnit>();
		for(WargusPlayer p:m_opponents) 
			for(WargusUnit u:p.getUnits()) l.add(u);
		return l;
	}
	
	public float getPlayerStrength(int playerID) {
		if (m_player.getID()==playerID) {
			return m_player.getUnits().size();
		} else {
			for(WargusPlayer p:m_opponents) 
				if (p.getID()==playerID) return p.getUnits().size();			
		}
		return 0;
	}
	
	public float distance(WargusGameState s,float mapwt,float troopwt,float buildingwt, float resourcewt) {
		if (m_metrics_selfB==null) m_metrics_selfB = new MetricBuildings(m_player);
		if (m_metrics_oppB==null) {
			m_metrics_oppB = new MetricBuildings();
			for(WargusPlayer opp:m_opponents) {
				m_metrics_oppB = new MetricBuildings(opp,m_metrics_oppB);
			} 
		}
		if (m_metrics_selfR==null) m_metrics_selfR = new MetricResources(m_player);
		if (m_metrics_oppR==null) {
			m_metrics_oppR = new MetricResources();
			for(WargusPlayer opp:m_opponents) {
				m_metrics_oppR = new MetricResources(opp,m_metrics_oppR);
			} 
		}
		if (m_metrics_selfT==null) m_metrics_selfT = new MetricTroops(m_player);
		if (m_metrics_oppT==null) {
			m_metrics_oppT = new MetricTroops();
			for(WargusPlayer opp:m_opponents) {
				m_metrics_oppT = new MetricTroops(opp,m_metrics_oppT);
			}
		}
		
		if (s.m_metrics_selfB==null) s.m_metrics_selfB = new MetricBuildings(s.m_player);
		if (s.m_metrics_oppB==null) {
			s.m_metrics_oppB = new MetricBuildings();
			for(WargusPlayer opp:s.m_opponents) {
				s.m_metrics_oppB = new MetricBuildings(opp,s.m_metrics_oppB);
			} 
		}
		if (s.m_metrics_selfR==null) s.m_metrics_selfR = new MetricResources(s.m_player);
		if (s.m_metrics_oppR==null) {
			s.m_metrics_oppR = new MetricResources();
			for(WargusPlayer opp:s.m_opponents) {
				s.m_metrics_oppR = new MetricResources(opp,s.m_metrics_oppR);
			} 
		}
		if (s.m_metrics_selfT==null) s.m_metrics_selfT = new MetricTroops(s.m_player);
		if (s.m_metrics_oppT==null) {
			s.m_metrics_oppT = new MetricTroops();
			for(WargusPlayer opp:s.m_opponents) {
				s.m_metrics_oppT = new MetricTroops(opp,s.m_metrics_oppT);
			}
		}		
		
		float d1 = distanceMap(s);
		float d2 = distanceTroops(s);
		float d3 = distanceBuildings(s);
		float d4 = distanceResources(s);
		float d_description = mapwt*d1 + troopwt*d2 + buildingwt*d3 + resourcewt*d4;
		
		//System.out.println("WargusGameState: distance is " + d1 + "," + d2 + "," + d3 + "," + d4 + ": " + d_description);
		
		return d_description;
	}
	
	
	/*
	 * Calculating distance between the map in the current Case Base Object 
	 * and the map within the case base object
	 * passed. This helps in measuring similarity between the current map state during game execution 
	 * and the map state that was present when the given plan in this 
	 * case object was used by the expert
	 */	
	public float distanceMap(WargusGameState s){
		float d_size;
		float d_landmetric;
		float d_watermetric;
		float d_treemetric;
		
		d_size = Math.abs((m_map.get_width()*m_map.get_height()) - 
				     	  (s.m_map.get_width()*s.m_map.get_height())) / 
				          (16384-1024);
		
		d_landmetric = Math.abs((m_map.getLandArea()/
								(m_map.get_width()*m_map.get_height()))
									-
								(s.m_map.getLandArea()/
								(s.m_map.get_width()*s.m_map.get_height()))
								); 
		d_watermetric = Math.abs((m_map.getWaterArea()/
				(m_map.get_width()*m_map.get_height()))
					-
				(s.m_map.getWaterArea()/
				(s.m_map.get_width()*s.m_map.get_height()))
				); 

				          
		d_treemetric = Math.abs((m_map.getTreeArea()/
				(m_map.get_width()*m_map.get_height()))
					-
				(s.m_map.getTreeArea()/
				(s.m_map.get_width()*s.m_map.get_height()))
				); 
						
		return (d_size + d_treemetric + d_landmetric + d_watermetric)/4 ;
	}
	
	/*
	 * Similar Distance calculation for the attacking units
	 * 
	 */
	public float distanceTroops(WargusGameState s){
		float d_numFighter;
		float d_numArcher;
		float d_numPeasant;
		float d_numGroundTroops;
		float d_numAirTroops;
		float d_numOceanTroops;
				
		d_numFighter = Math.abs((m_metrics_selfT.getnumFighter()+1)/(m_metrics_selfT.getnumFighter()+m_metrics_oppT.getnumFighter()+2) - 
								(s.m_metrics_selfT.getnumFighter()+1)/(s.m_metrics_selfT.getnumFighter()+s.m_metrics_oppT.getnumFighter()+2));
		d_numArcher = Math.abs((m_metrics_selfT.getnumArcher()+1)/(m_metrics_selfT.getnumArcher()+m_metrics_oppT.getnumArcher()+2) - 
				(s.m_metrics_selfT.getnumArcher()+1)/(s.m_metrics_selfT.getnumArcher()+s.m_metrics_oppT.getnumArcher()+2));
		d_numPeasant = Math.abs((m_metrics_selfT.getnumPeasant()+1)/(m_metrics_selfT.getnumPeasant()+m_metrics_oppT.getnumPeasant()+2) - 
				(s.m_metrics_selfT.getnumPeasant()+1)/(s.m_metrics_selfT.getnumPeasant()+s.m_metrics_oppT.getnumPeasant()+2));
		d_numGroundTroops = Math.abs((m_metrics_selfT.getnumGroundTroops()+1)/(m_metrics_selfT.getnumGroundTroops()+m_metrics_oppT.getnumGroundTroops()+2) - 
				(s.m_metrics_selfT.getnumGroundTroops()+1)/(s.m_metrics_selfT.getnumGroundTroops()+s.m_metrics_oppT.getnumGroundTroops()+2));
		d_numAirTroops = Math.abs((m_metrics_selfT.getnumAirTroops()+1)/(m_metrics_selfT.getnumAirTroops()+m_metrics_oppT.getnumAirTroops()+2) - 
				(s.m_metrics_selfT.getnumAirTroops()+1)/(s.m_metrics_selfT.getnumAirTroops()+s.m_metrics_oppT.getnumAirTroops()+2));
		d_numOceanTroops = Math.abs((m_metrics_selfT.getnumOceanTroops()+1)/(m_metrics_selfT.getnumOceanTroops()+m_metrics_oppT.getnumOceanTroops()+2) - 
				(s.m_metrics_selfT.getnumOceanTroops()+1)/(s.m_metrics_selfT.getnumOceanTroops()+s.m_metrics_oppT.getnumOceanTroops()+2));
		
		return((d_numFighter + 
				d_numArcher + 
				d_numPeasant + 
				d_numGroundTroops + 
				d_numAirTroops + 
				d_numOceanTroops)/6);
	}
	
	/*
	 * Similar Distance calculation for the available buildings 
	 */
	public float distanceBuildings(WargusGameState s){
		float d_numTownHalls;
		float d_numBarracks;
		float d_numLumberMills;
		float d_numBlackSmith;
		float d_numAdvancedBuilding;
		float d_numFarms;
		float d_numTowers;
				
		d_numTownHalls = Math.abs((m_metrics_selfB.getnumTownHalls()+1)/(m_metrics_selfB.getnumTownHalls()+m_metrics_oppB.getnumTownHalls()+2) - 
								  (s.m_metrics_selfB.getnumTownHalls()+1)/(s.m_metrics_selfB.getnumTownHalls()+s.m_metrics_oppB.getnumTownHalls()+2));

		d_numBarracks = Math.abs((m_metrics_selfB.getnumBarracks()+1)/(m_metrics_selfB.getnumBarracks()+m_metrics_oppB.getnumBarracks()+2) - 
				  				(s.m_metrics_selfB.getnumBarracks()+1)/(s.m_metrics_selfB.getnumBarracks()+s.m_metrics_oppB.getnumBarracks()+2));
		d_numLumberMills = Math.abs((m_metrics_selfB.getnumLumberMills()+1)/(m_metrics_selfB.getnumLumberMills()+m_metrics_oppB.getnumLumberMills()+2) - 
				  				(s.m_metrics_selfB.getnumLumberMills()+1)/(s.m_metrics_selfB.getnumLumberMills()+s.m_metrics_oppB.getnumLumberMills()+2));
		d_numBlackSmith = Math.abs((m_metrics_selfB.getnumBlackSmith()+1)/(m_metrics_selfB.getnumBlackSmith()+m_metrics_oppB.getnumBlackSmith()+2) - 
				  				(s.m_metrics_selfB.getnumBlackSmith()+1)/(s.m_metrics_selfB.getnumBlackSmith()+s.m_metrics_oppB.getnumBlackSmith()+2));
		d_numAdvancedBuilding = Math.abs((m_metrics_selfB.getnumAdvancedBuilding()+1)/(m_metrics_selfB.getnumAdvancedBuilding()+m_metrics_oppB.getnumAdvancedBuilding()+2) - 
				  				(s.m_metrics_selfB.getnumAdvancedBuilding()+1)/(s.m_metrics_selfB.getnumAdvancedBuilding()+s.m_metrics_oppB.getnumAdvancedBuilding()+2));
		d_numFarms = Math.abs((m_metrics_selfB.getnumFarms()+1)/(m_metrics_selfB.getnumFarms()+m_metrics_oppB.getnumFarms()+2) - 
				  				(s.m_metrics_selfB.getnumFarms()+1)/(s.m_metrics_selfB.getnumFarms()+s.m_metrics_oppB.getnumFarms()+2));
		d_numTowers = Math.abs((m_metrics_selfB.getnumTowers()+1)/(m_metrics_selfB.getnumTowers()+m_metrics_oppB.getnumTowers()+2) - 
				  				(s.m_metrics_selfB.getnumTowers()+1)/(s.m_metrics_selfB.getnumTowers()+s.m_metrics_oppB.getnumTowers()+2));
				
		return(( d_numTownHalls +
				d_numBarracks +
				d_numLumberMills + 
				d_numBlackSmith +
				d_numAdvancedBuilding + 
				d_numFarms + 
				d_numTowers)/7) ;
	}
	
	/*
	 * Similar Distance calculation for the available resources
	 */
	public float distanceResources(WargusGameState s){
		float d_gold;
		float d_wood;
		float d_oil;
		float d_food;
		
		d_gold = Math.abs((m_metrics_selfR.getGold()+1)/(m_metrics_selfR.getGold()+m_metrics_oppR.getGold()+2) - 
						  (s.m_metrics_selfR.getGold()+1)/(s.m_metrics_selfR.getGold()+s.m_metrics_oppR.getGold()+2));
		
		d_wood = Math.abs((m_metrics_selfR.getWood()+1)/(m_metrics_selfR.getWood()+m_metrics_oppR.getWood()+2) - 
				  		(s.m_metrics_selfR.getWood()+1)/(s.m_metrics_selfR.getWood()+s.m_metrics_oppR.getWood()+2));
		d_oil = Math.abs((m_metrics_selfR.getOil()+1)/(m_metrics_selfR.getOil()+m_metrics_oppR.getOil()+2) - 
				  		(s.m_metrics_selfR.getOil()+1)/(s.m_metrics_selfR.getOil()+s.m_metrics_oppR.getOil()+2));
		d_food = Math.abs((m_metrics_selfR.getFood()+1)/(m_metrics_selfR.getFood()+m_metrics_oppR.getFood()+2) - 
				  		(s.m_metrics_selfR.getFood()+1)/(s.m_metrics_selfR.getFood()+s.m_metrics_oppR.getFood()+2));
				
		return ((d_gold + d_wood + d_oil + d_food)/4);
	}	
	
	
	public WargusMap getMap() {
		return m_map;
	}
	
	public List<WargusPlayer> getOpponent() {
		return m_opponents;
	}

	public WargusPlayer getPlayer() {
		return m_player;
	}
	
	public List<WargusPlayer> getPlayers() {
		List<WargusPlayer> l = new LinkedList<WargusPlayer>();
		
		l.add(m_player);
		l.addAll(m_opponents);
		return l;
	}	
	
	/*
	 * This method saves an episode to a file in XML format
	 */
	public void writeXML(XMLWriter w) {
		w.tag("description");		
		{
			m_map.writeXML(w);
			
			w.tag("cbr-player");
			{
				m_player.writeXML(w);
			}
			w.tag("/cbr-player");
			
			w.tag("other-players");			
			{
				for(WargusPlayer p:m_opponents) p.writeXML(w);
			}
			w.tag("/other-players");
		}
		w.tag("/description");
	}
	public static void writeXML(XMLWriter w, WargusGameState wgs) {
		if (wgs == null) {
			w.tag("description", "");
		} else {
			wgs.writeXML(w);
		}
	}
	
	public static WargusGameState loadFromXML(Element e) {
		assert e.getName().equals("description") : 
			"WargusGameState.loadFromXML: Invalid XML Element " + e.getName();

		if (e.getContent().size() == 0) return null;
		
		WargusMap map = WargusMap.loadFromXML(e.getChild("map"));
		WargusPlayer player = WargusPlayer.loadFromXML(e.getChild("cbr-player").getChild("player"));
		List<WargusPlayer> opponents = new LinkedList<WargusPlayer>();		
		for (Object o : e.getChild("other-players").getChildren("player")) {
			Element playerElement = (Element) o;
			opponents.add(WargusPlayer.loadFromXML(playerElement));
		}
		return new WargusGameState(map, opponents, player);
	}


	/*
	 * Given a node import the XML into an internal Case Base Episode
	 */
	public static WargusGameState ImportXMLGameState(Node descriptionNode,int playerID) {
		WargusMap map = null;
		List<WargusPlayer> opponents = new LinkedList<WargusPlayer>();
		WargusPlayer player = null;
		
		// Load map:
		if (descriptionNode!=null) {
			Node mapNode = CBLILRModule.XMLfeatureValue(descriptionNode, "map");
			if (mapNode!=null) {
				map = WargusMap.ImportXMLMap(mapNode);
			}
		}
		
		// Load players:
		if (descriptionNode!=null) {
			Node playersNode = CBLILRModule.XMLfeatureValue(descriptionNode, "players");
			if (playersNode!=null) {
				NodeList playersNodeList = playersNode.getChildNodes();
				int l = playersNodeList.getLength();
				int i;
				for(i=0;i<l;i++) {
					Node playerNode = playersNodeList.item(i);
					
					if (playerNode.getNodeName().equals("player")) {						
						opponents.add(WargusPlayer.ImportXMLPlayer(playerNode));
					}
				}
			}
		}
		
		for(WargusPlayer p:opponents) {
			if (p.getID() == playerID) player = p;
		}
		if (player!=null) opponents.remove(player);
		
		if (player==null) {
			System.out.println("ImportXMLGameState: null player!!! (ID = " + playerID + ")");
			throw new Error();
		}
	
				
		return new WargusGameState(map,opponents,player);
	}		

}
